Nestjs 使用 graphql

基本使用

安装依赖

bash
1pnpm i @nestjs/graphql @nestjs/apollo @apollo/server graphql

在 AppModule 中引入

typescript
1import { Module } from '@nestjs/common'
2import { AppController } from './app.controller'
3import { AppService } from './app.service'
4import { GraphQLModule } from '@nestjs/graphql'
5import { ApolloDriver } from '@nestjs/apollo'
6import { ApolloServerPluginLandingPageLocalDefault } from '@apollo/server/plugin/landingPage/default'
7
8@Module({
9  imports: [
10    GraphQLModule.forRoot({
11      driver: ApolloDriver,
12      playground: false,
13      plugins: [ApolloServerPluginLandingPageLocalDefault()],
14      typePaths: ['./**/*.graphql'],
15    }),
16  ],
17  controllers: [AppController],
18  providers: [AppService],
19})
20export class AppModule {}
  • driver:使用 ApolloDriver 作为驱动程序
  • playground:是否使用 Graphql-playground,这里禁用掉,因为 Apollo-sandbox 更好用
  • plugins:插件,使用 ApolloServerPluginLandingPageLocalDefault 开启 Apollo-sandbox
  • typePaths :编写的 GraphQL Schema 文件的路径

创建 Resource

bash
1nest g resource --no-spec
2? What name would you like to use for this resource (plural, e.g.,
3"users")? users
4? What transport layer do you use? GraphQL (schema first)
5? Would you like to generate CRUD entry points? Yes

resource 能够生成一个比较完整的 CRUD 示例,包含 graphql schema、CRUD resolver 等等。

生成后会创建这些文件。

bash
1CREATE src/users/users.graphql (397 bytes)
2CREATE src/users/users.module.ts (225 bytes)
3CREATE src/users/users.resolver.ts (959 bytes)
4CREATE src/users/users.service.ts (625 bytes)
5CREATE src/users/dto/create-user.input.ts (32 bytes)
6CREATE src/users/dto/update-user.input.ts (192 bytes)
7CREATE src/users/entities/user.entity.ts (21 bytes)

更新 app.module.ts把 UsersModule 引入进来。

diff
1imports: [
2    GraphQLModule.forRoot(...),
3+    UserModule,
4  ],

自动生成 type 的脚本

安装 ts-morph

bash
1pnpm install ts-morph

在根目录创建gen-typings.ts文件并写入:

typescript
1import { GraphQLDefinitionsFactory } from '@nestjs/graphql'
2import { join } from 'path'
3
4const definitionsFactory = new GraphQLDefinitionsFactory()
5definitionsFactory.generate({
6  typePaths: ['./src/**/*.graphql'],
7  path: join(process.cwd(), 'src/graphql.ts'),
8})

在 package.json 中的 scripts 加上:

json
1"gen-typings": "ts-node gen-typings.ts"

执行 pnpm run gen-typings将生成类型文件。

image-20240504011734027

进入http://localhost:3000/graphql用 Apollo-sandbox 查询:

image-20240504012050534

传递额外的 context

每个 resolver 都会收到的 context 是 req ,如果想要额外添加则可以在GraphQLModule.forRoot中传递:

typescript
1    GraphQLModule.forRoot({
2    ...
3      context: () => ({ data: '123' }),
4    }),

在 Query 中通过 @Context装饰器拿到它并打印出来:

typescript
1  @Query('users')
2  findAll(@Context() ctx: any) {
3    console.log('——————🚀🚀🚀🚀🚀 —— findAll —— ctx.data:', ctx.data);
4
5    return this.usersService.findAll();
6  }

directive

定义指令标识符

scheme
1// directive.graphql
2directive @upper on FIELD_DEFINITION
3directive @auth on FIELD_DEFINITION

定义指令 transformer:

示例 1:将 string 结果大写

typescript
1// upper.transformer.ts
2import { getDirective, MapperKind, mapSchema } from '@graphql-tools/utils'
3import { defaultFieldResolver, GraphQLSchema } from 'graphql'
4
5export function upperDirectiveTransformer(schema: GraphQLSchema, directiveName: string) {
6  return mapSchema(schema, {
7    [MapperKind.OBJECT_FIELD]: (fieldConfig) => {
8      const upperDirective = getDirective(schema, fieldConfig, directiveName)?.[0]
9
10      if (upperDirective) {
11        const { resolve = defaultFieldResolver } = fieldConfig
12
13        fieldConfig.resolve = async function (source, args, context, info) {
14          const result = await resolve(source, args, context, info)
15          if (typeof result === 'string') {
16            return result.toUpperCase()
17          }
18          return result
19        }
20        return fieldConfig
21      }
22    },
23  })
24}

示例 2:传递 auth 给 context

typescript
1import { getDirective, MapperKind, mapSchema } from '@graphql-tools/utils'
2import { defaultFieldResolver, GraphQLSchema } from 'graphql'
3
4export function authDirectiveTransformer(schema: GraphQLSchema, directiveName: string) {
5  return mapSchema(schema, {
6    [MapperKind.OBJECT_FIELD]: (fieldConfig) => {
7      const authDirective = getDirective(schema, fieldConfig, directiveName)?.[0]
8
9      if (authDirective) {
10        const { resolve = defaultFieldResolver } = fieldConfig
11
12        fieldConfig.resolve = async function (source, args, context, info) {
13          const result = await resolve(source, args, { ...context, auth: 'auth' }, info)
14
15          return result
16        }
17        return fieldConfig
18      }
19    },
20  })
21}

GraphQLModule.forRoot中装载:

typescript
1    GraphQLModule.forRoot({
2      ...
3      context: () => ({ data: '123' }),
4      transformSchema: (originalSchema) => {
5        let schema = originalSchema;
6
7        schema = upperDirectiveTransformer(schema, 'upper');
8        schema = authDirectiveTransformer(schema, 'auth');
9
10        return schema;
11      },
12    }),

新建一个 Query 查询:

typescript
1type Query {
2  userName: String! @upper @auth
3}

在 resolver 中写处理方法:

typescript
1  @Query('userName')
2  async userName(@Context() ctx: any): Promise<string> {
3    console.log('——————🚀🚀🚀🚀🚀 —— getUserName —— Context:', ctx);
4    return ctx.auth;
5  }

用 Apollo-sandbox 查询一下:

image-20240506224653247

指令成功运行,拿到 context 并且将结果大写了。